iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
JavaScript

從 JavaScript 到 TypeScript:30 天建立 AI 時代的型別思維系列 第 11 篇

day11|型別縮小:讓條件判斷幫忙確認型別

  • 分享至 

  • xImage
  •  

前一篇用聯集列出題型允許的值。當一個變數有多種可能時,使用它的程式還得確認目前拿到哪一種。假設作答資料可能是一段文字,也可能是一組選項。

型別縮小(narrowing)是 TypeScript 根據條件判斷與程式路徑,推算某個位置還可能出現哪些型別。判斷式會在 JavaScript 執行時檢查值;TypeScript 則在編譯時讀取這些判斷,幫我們檢查每條路徑上的操作。

用 typeof 分開處理答案

下面的聯集表示答案可能是字串或字串陣列。因為字串沒有 join()。先檢查答案是否為字串,就能分開處理:

type SubmittedAnswer = string | string[];

function answerText(answer: SubmittedAnswer): string {
  if (typeof answer === "string") {
    return answer;
  }

  return answer.join("、");
}

typeof 會回傳值在執行時的型別名稱,TypeScript 可依判斷結果縮小聯集。更多範例可參考 TypeScript Handbook 的型別縮小章節。

SubmittedAnswer 是 string 或 string[];經過 typeof answer === "string" 判斷後,成立的路徑只處理字串,另一條路徑只處理字串陣列。

typeof null 也會得到 "object"

對 null 和陣列使用 typeof,得到的都是 "object"。因此,對 string[] | null 使用 typeof items === "object",仍會留下 null;直接讀取陣列元素可能出錯。

這裡要辨識陣列,可以使用 Array.isArray():

function firstItem(items: string[] | null): string | undefined {
  if (Array.isArray(items)) {
    return items[0];
  }

  return undefined;
}

用 in 檢查物件有哪些欄位

當聯集的兩種物件各有不同的欄位,可以用 in 判斷欄位是否存在:

type QuestionContent =
  | { options: string[] }
  | { answer: string };

function describeQuestion(question: QuestionContent): string {
  if ("options" in question) {
    return `選項:${question.options.join("、")}`;
  }

  return `填空答案:${question.answer}`;
}

"options" in question 會檢查物件本身或原型鏈上是否有這個欄位,因此能區分這裡的兩種物件。若兩種物件都把 options 設為可選欄位,還需要檢查其他能區分它們的欄位。

用 instanceof 判斷實例

instanceof 用來判斷一個值是否為指定類別的實例。下面的值可能是 Date 物件或字串;判斷後,TypeScript 就能在各分支縮小型別:

function dateLabel(value: Date | string): string {
  if (value instanceof Date) {
    return value.toISOString().slice(0, 10);
  }

  return value.trim();
}

instanceof Date 成立後,TypeScript 會把 value 視為 Date,因此可以使用 Date 的方法,例如 toISOString()。

直接用值當條件,會排除哪些值?

直接把值放進 if 也能縮小型別,條件會依 JavaScript 的真假值(truthiness)判斷。""、0、null 和 undefined 都是假值。作答資料是 string | undefined 時,if (answer) 會把空字串和未作答一起排除;若空字串仍算一次作答,就要明確檢查 undefined:

function answerStatus(answer: string | undefined): string {
  if (answer !== undefined) {
    return `已作答:${answer}`;
  }

  return "尚未作答";
}

console.log(answerStatus("")); // "已作答:"

如果產品把空字串也當成「尚未作答」,if (answer) 才符合需求。空陣列和空物件都是真值;要檢查陣列裡有沒有項目,應看 length。

return 也會改變後面的可能性

TypeScript 會分析程式能走到哪裡,這稱為控制流程分析(control-flow analysis)。每個提早結束的分支,都會讓後面的程式少一種可能:

function summarize(value: string | number | null): string {
  if (value === null) {
    return "沒有結果";
  }

  if (typeof value === "string") {
    return value.trim();
  }

  return value.toFixed(2);
}

第一個 return 處理了 null;第二個處理了字串。最後一行能執行時,value 只剩數字。若不同分支都繼續往下走,TypeScript 會把那些路徑重新合併,再計算當下可能的型別。

條件判斷也能用來檢查外部資料,但檢查內容要符合實際需求。例如 typeof value === "object" 會接受 null,也沒有確認物件有哪些欄位;單靠它無法驗證一筆完整題目。型別縮小只根據已寫出的條件推導,資料是否可信取決於檢查是否足夠。

今日練習

day11 | 型旅 TypeTrail

結論

聯集列出所有可能,條件判斷讓程式確認目前拿到哪一種值。選擇 typeof、in、instanceof 或明確比較時,要先想清楚條件在執行時究竟檢查了什麼。資料有不同結構時,明確的辨識欄位能讓分支判斷更容易讀懂。


本文同步分享於個人部落格:https://johnsonchen.dev/blog/typescript/type-narrowing/


上一篇
day10|字面值與聯集:用型別表達業務規則
下一篇
day12|可辨識聯集:一個欄位決定要帶哪些資料
系列文
從 JavaScript 到 TypeScript:30 天建立 AI 時代的型別思維 共 13 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言